// Just takin a ghostly walk through a forest! Creepy.

#ifdef __APPLE_CC__
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <list>

using namespace std;

// Sets the FPS for the scene
static const int FPS = 60;

// Set the number of trees to plant
static const int TREE_COUNT = 100;

// Colors
GLfloat WHITE[] = {1, 1, 1};
GLfloat RED[] = {1, 0, 0};
GLfloat TREE_GREEN[] = {0, 1, 0};
GLfloat GRASS[] = {0.23, 0.32, 0.12};
GLfloat BARK_BROWN[] = {0.38, 0.20, 0.08};
GLfloat BLACK[] = {0.0, 0.0, 0.0, 1.0};

// Fog stuff
GLfloat FOG_DENS = 0.05;

// Miscellaneous vars
static const float PI = 3.141592;
static float cameraBob = 0.0;

// A camera that moves around the forest
class Camera {
    double x;
    double y;
    double z;
    double targetX;
    double targetY;
    double targetZ;
    double theta;
    double dTheta;

    // Helper method to update what the camera is looking at
    void updateTarget() {
        targetX = x + 0.5 * cos(theta);
        targetZ = z + 0.5 * sin(theta);
        targetY = y;
    }

public:
    // Camera starts at origin, slightly hovering above the ground,
    // with eye towards the positive x axis
    Camera(): x(0), y(3), z(0),
              targetX(1.0), targetY(3), targetZ(0.0), theta(0), dTheta(0.05) {}

    // Getters for the camera
    double getX() {return x;}
    double getY() {return y;}
    double getZ() {return z;}
    double getTargetX() {return targetX;}
    double getTargetY() {return targetY;}
    double getTargetZ() {return targetZ;}
    double getTheta() {return theta;}

    // Moving the camera
    void turnRight() {theta += dTheta; updateTarget();}
    void turnLeft() {theta -= dTheta; updateTarget();}
    void moveForward() {x += 0.5 * cos(theta); z += 0.5 * sin(theta); updateTarget();}
    void moveBack() {x -= 0.5 * cos(theta); z -= 0.5 * sin(theta); updateTarget();}
    void setY(double y0) {y = y0; updateTarget();}
};

// Custom tree class to get all Arbor Day up in this.
class Tree {
    int displayListId;
    double x;
    double z;
public:
    // Create a tree! Random values passed in (presumably)
    void create(double x0, double z0) {
        x = x0;
        z = z0;
        displayListId = glGenLists(1);
        glNewList(displayListId, GL_COMPILE);
            // Create the trunk
            glPushMatrix();
                glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, BARK_BROWN);
                glColor3fv(BARK_BROWN);
                glRotatef(-90, 1.0, 0.0, 0.0);
                gluCylinder(gluNewQuadric(), 0.7, 0.5, 4.0, 20.0, 20.0);
            glPopMatrix();

            // Create the leaves
            glPushMatrix();
                glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, TREE_GREEN);
                glColor3fv(TREE_GREEN);
                glTranslatef(0.0, 6.8, 0.0);
                glutSolidSphere(3.0, 20.0, 20.0);
            glPopMatrix();
        glEndList();
    }

    void draw() {
        // Perform the translation of the tree onto the plane
        // based on its x and z coordinates
        glPushMatrix();
            glTranslatef(x, 0, z);
            glCallList(displayListId);
        glPopMatrix();
    }
};

// Custom ground class to easily draw the floor.
class Ground {
    int displayListId;
    int width;
    int depth;
public:
    double centerx() {return width / 2;}
    double centerz() {return depth / 2;}

    Ground(int width, int depth): width(width), depth(depth) {}
    void create() {
        displayListId = glGenLists(1);
        glNewList(displayListId, GL_COMPILE);
        glBegin(GL_QUADS);
            glColor3fv(GRASS);
            glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, GRASS);
            glNormal3d(0, 1, 0);
            glVertex3d(-centerx(), 0, centerz());
            glVertex3d(-centerx(), 0, -centerz());
            glVertex3d(centerx(), 0, -centerz());
            glVertex3d(centerx(), 0, centerz());
        glEnd();
        glEndList();
    }

    void draw() {
        glPushMatrix();
        glCallList(displayListId);
        glPopMatrix();
    }
};

// Global variables: a camera, some trees, and the ground.
Camera camera;
Ground ground(128, 128); //Powers of two ftw
list<Tree> trees;

// Application-specific initialization: Set up global lighting parameters
// and create display lists.
void init() {
    glEnable(GL_DEPTH_TEST);
    ground.create();

    // Set up the perspective
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60.0, 4/3, 1.0, 150.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

   // Configure the flashlight for the current position
    GLfloat pos[] = {0.0, 0.0, 0.0, 1.0};
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT1);
    glEnable(GL_COLOR_MATERIAL);
    glLightfv(GL_LIGHT1, GL_AMBIENT, BLACK);
    glLightfv(GL_LIGHT1, GL_DIFFUSE, WHITE);
    glLightfv(GL_LIGHT1, GL_POSITION, pos);
    glLightf(GL_LIGHT1, GL_SPOT_EXPONENT, 128.0);
    glLightf(GL_LIGHT1, GL_SPOT_CUTOFF, 3.0);
    glLightf(GL_LIGHT1, GL_CONSTANT_ATTENUATION, 0);
    glLightf(GL_LIGHT1, GL_LINEAR_ATTENUATION, 0.075);
    glLightf(GL_LIGHT1, GL_QUADRATIC_ATTENUATION, 0);

    // Plant some trees!
    for (int i = 0; i < TREE_COUNT; i++) {
        Tree t;
        float randomX = (float) rand()/RAND_MAX;
        float randomZ = (float) rand()/RAND_MAX;
        t.create(-ground.centerx() + 2 * (ground.centerx() * randomX),
                 -ground.centerz() + 2 * (ground.centerz() * randomZ));
        trees.push_back(t);
    }

    // Fog! Now it's getting creepy...
    glEnable(GL_FOG);
    glFogi(GL_FOG_MODE, GL_EXP2);
    glFogfv(GL_FOG_COLOR, BLACK);
    glFogf(GL_FOG_DENSITY, FOG_DENS);
    glHint(GL_FOG_HINT, GL_NICEST);
}

// Updates the current display
void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    gluLookAt(camera.getX(), camera.getY(), camera.getZ(),
        camera.getTargetX(), camera.getTargetY(), camera.getTargetZ(),
        0.0, 1.0, 0.0);
    ground.draw();

    // Go through the tree list, drawing each in turn
    for (list<Tree>::iterator i = trees.begin(); i != trees.end(); i++) {
        i->draw();
    }

    glFlush();
    glutSwapBuffers();
}

// On reshape, constructs a camera that perfectly fits the window.
void reshape(GLint w, GLint h) {
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60.0, GLfloat(w) / GLfloat(h), 1.0, 150.0);
    glMatrixMode(GL_MODELVIEW);
}

// Requests to draw the next frame.
void timer(int v) {
    // Set the camera to bob up and down... like a ghost! OOOoooo
    camera.setY(3 + sin(cameraBob));
    cameraBob -= 0.01;
    if (cameraBob < -2 * PI) {
        cameraBob = 2 * PI;
    }
    glutPostRedisplay();
    glutTimerFunc(1000 / FPS, timer, v);
}

// Moves the camera according to the key pressed, then ask to refresh the
// display.
void special(int key, int, int) {
    switch (key) {
        case GLUT_KEY_LEFT: camera.turnLeft(); break;
        case GLUT_KEY_RIGHT: camera.turnRight(); break;
        case GLUT_KEY_UP: camera.moveForward(); break;
        case GLUT_KEY_DOWN: camera.moveBack(); break;
    }
    glutPostRedisplay();
}

// Initializes GLUT and enters the main loop.
int main(int argc, char** argv) {
    srand(time(NULL));
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
    glutInitWindowPosition(80, 80);
    glutInitWindowSize(800, 600);
    glutCreateWindow("A Ghostly Forest Walk");
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutSpecialFunc(special);
    glutTimerFunc(100, timer, 0);
    init();
    glutMainLoop();
}
